#include <stdio.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
#include <libavcodec/avcodec.h>
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersrc.h>
#include <libavfilter/buffersink.h>
#include <libavformat/avformat.h>
static AVFormatContext *fmt_ctx = NULL;
static AVCodecContext *dec_ctx = NULL;
AVFilterGraph *graph = NULL;
AVFilterContext *buf_ctx = NULL;
AVFilterContext *buf_sink_ctx = NULL;
static int v_stream_index = -1;
static int do_frame(AVFrame *filter_frame, FILE *out) {
  fwrite(filter_frame->data[0], 1, filter_frame->width * filter_frame->height, out);
  fflush(out);
  return 0;
}
static int filter_video(AVFrame *frame, AVFrame *filter_frame, FILE *out) {
  int ret;
  ret = av_buffersrc_add_frame(buf_ctx, frame);
  if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "failed to feed to filter graph! \n");
    return ret;
  }
  while (1) {
    ret = av_buffersink_get_frame(buf_sink_ctx, filter_frame);
    if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) {
      break;
    }
    if (ret < 0) {
      return ret;
    }
    do_frame(filter_frame, out);
    av_frame_unref(filter_frame);
  }
  av_frame_unref(frame);
  return 0;
}
static int decode_frame_and_filter(AVFrame *frame, AVFrame *filter_frame, FILE *out) {
  int ret = avcodec_receive_frame(dec_ctx, frame);

  if (ret < 0) {
    if (ret != AVERROR_EOF && ret != AVERROR(EAGAIN)) {
      av_log(NULL, AV_LOG_ERROR, "error while receiving a frame from decoder \n");
    }
    return ret;
  }
  return filter_video(frame, filter_frame, out);
}
static int open_input_file(const char *filename) {

  int ret = -1;
  const AVCodec *dec = NULL;
  AVStream *inStream = NULL;
  ret = avformat_open_input(&fmt_ctx, filename, NULL, NULL);
  if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "failed to open file %s\n", filename);
    return ret;
  }
  ret = avformat_find_stream_info(fmt_ctx, NULL);
  if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "failed to find stream information! \n");
    return ret;
  }
  v_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &dec, 0);
  if (v_stream_index < 0) {
    av_log(NULL, AV_LOG_ERROR, "can not find video stream! \n");
    return ret;
  }

  dec_ctx = avcodec_alloc_context3(dec);
  if (!dec_ctx) {
    return AVERROR(ENOMEM);
  }

  avcodec_parameters_to_context(dec_ctx, fmt_ctx->streams[v_stream_index]->codecpar);

  ret = avcodec_open2(dec_ctx, dec, NULL);
  if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "failed to open decoder! \n");
    return ret;
  }

  return 0;
}

static int init_filters(const char *filter_desc) {
  int ret = -1;
  char args[512] = {};

  AVRational time_base = fmt_ctx->streams[v_stream_index]->time_base;
  AVFilterInOut *inputs = avfilter_inout_alloc();
  AVFilterInOut *outputs = avfilter_inout_alloc();
  if (!inputs || !outputs) {
    av_log(NULL, AV_LOG_ERROR, "no memory\n");
    return AVERROR(ENOMEM);
  }

  graph = avfilter_graph_alloc();
  if (!graph) {
    av_log(NULL, AV_LOG_ERROR, "failed to create graph\n");
    return AVERROR(ENOMEM);
  }
  const AVFilter *buf_sink = avfilter_get_by_name("buffersink");
  if (!buf_sink) {
    av_log(NULL, AV_LOG_ERROR, "failed to get buffer sink filter\n");
    return -1;
  }

  const AVFilter *buf_src = avfilter_get_by_name("buffer");
  if (!buf_src) {
    av_log(NULL, AV_LOG_ERROR, "failed to get buffer filter \n");
    return -1;
  }
  snprintf(args, 512, "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
           dec_ctx->width,
           dec_ctx->height,
           dec_ctx->pix_fmt,
           time_base.num,
           time_base.den,
           dec_ctx->sample_aspect_ratio.num,
           dec_ctx->sample_aspect_ratio.den
  );
  ret = avfilter_graph_create_filter(&buf_ctx, buf_src, "in", args, NULL, graph);
  if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "failed to create buffer  filter context \n");
    goto __ERROR;
  }
  enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_GRAY8, AV_PIX_FMT_NONE};
  ret = avfilter_graph_create_filter(&buf_sink_ctx, buf_sink, "out", NULL, NULL, graph);
  if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "failed to create buffer sink filter context \n");
    goto __ERROR;
  }

  av_opt_set_int_list(buf_sink_ctx, "pix_fmts",
                      pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);

  inputs->name = av_strdup("out");
  inputs->filter_ctx = buf_sink_ctx;
  inputs->pad_idx = 0;
  inputs->next = NULL;

  outputs->name = av_strdup("in");
  outputs->filter_ctx = buf_ctx;
  outputs->pad_idx = 0;
  outputs->next = NULL;

  ret = avfilter_graph_parse_ptr(graph, filter_desc, &inputs, &outputs, NULL);
  if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "failed to  parse graph filter description \n");
    goto __ERROR;
  }
  ret = avfilter_graph_config(graph, NULL);
  if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "failed to  config graph\n");
    goto __ERROR;
  }
  __ERROR:
  if (graph) {
    avfilter_graph_free(&graph);
  }
  avfilter_inout_free(&inputs);
  avfilter_inout_free(&outputs);
  return ret;
}

int main() {
  int ret = -1;
  AVPacket av_packet;
  AVFrame *frame = NULL;
  AVFrame *filter_frame = NULL;
  FILE *out = NULL;
  const char *filter_desc = "drawbox=30:10:64:64:red";
  const char *filename = "/Users/admin/Downloads/out.mp4";
  const char *outFile = "/Users/admin/Downloads/new.yuv";
  av_log_set_level(AV_LOG_DEBUG);
  frame = av_frame_alloc();
  filter_frame = av_frame_alloc();
  if (!frame || !filter_frame) {
    av_log(NULL, AV_LOG_ERROR, "failed to alloc frame memory! \n");
    goto __ERROR;
  }
  out = fopen(outFile, "wb");
  if (!out) {
    av_log(NULL, AV_LOG_ERROR, "failed to open yuv file! \n");
    exit(-1);
  }

  if (open_input_file(filename) < 0) {
    av_log(NULL, AV_LOG_ERROR, "failed to open media file! \n");
  } else {
    if (init_filters(filter_desc) < 0) {
      av_log(NULL, AV_LOG_ERROR, "failed to init filter! \n");
      goto __ERROR;
    }
  }
  printf("Hello, World!\n");

  while (1) {
    ret = av_read_frame(fmt_ctx, &av_packet);
    if (ret < 0) {
      break;
    }
    if (av_packet.stream_index == v_stream_index) {
      ret = avcodec_send_packet(dec_ctx, &av_packet);
      if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "failed to send packet to decoder ! \n");
        break;
      }
      ret = decode_frame_and_filter(frame, filter_frame, out);
      if (ret < 0) {
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
          continue;
        }
        av_log(NULL, AV_LOG_ERROR, "failed to decode or filter ! \n");
        break;
      }
    }
  }
  return 0;
  __ERROR:
  if (frame) {
    av_frame_free(&frame);
  }
  if (filter_frame) {
    av_frame_free(&filter_frame);
  }
  return -1;
}
